package com.google.code.virtualhockey.vhx.sds.fields

import com.google.code.virtualhockey.vhx.sds._
import com.google.appengine.api.datastore.Entity
import com.sun.corba.se.spi.activation._InitialNameServiceImplBase
import scala.annotation.target
import java.lang.reflect.{ParameterizedType, Field}

/**
 * Base class of all persistable fields.
 */
protected[sds] abstract class SField( val parent: STMetaData[_],
                                      val name: String,
                                      val indexed: Boolean = false,
                                      val dbName: Option[String] = None ) {

  /** Type of the described field.*/
  protected type TRawType

  /** Corresponding GAE type. */
  protected type TGaeType

  /** Class object for the raw field type. */
  protected val _clsRaw: Class[TRawType]

  /** Class object for the GAE field type. */
  protected val _clsGae: Class[TGaeType]

  /** Indicates whether this field should be shown in toString output. */
  protected[sds] val _showInToString: Boolean = true
  
  /** Indicates whether a value is optional or not (i.e. uses Option[ActualType]). */
  protected lazy val _isOptional: Boolean = {
    val f = parent.cls.getDeclaredField( name )
    if ( f.getType == classOf[Option[_]] ) {
      val aTypeParams = f.getGenericType.asInstanceOf[ParameterizedType].getActualTypeArguments()
      if ( aTypeParams.length == 1 && aTypeParams( 0 ) == _clsRaw )
        true
      else
        sys.error( "Field '" + name + "' is optional, but the type parameter is '" + aTypeParams( 0 ).asInstanceOf[Class[_]].getName + "'. Expected is '" + _clsRaw.getName + "'." )
    }
    else
      false
  }

  /** Underlying Java Field (reflection API) */
  protected lazy val _field: Field =
    try {
      val f = parent.cls.getDeclaredField( name )
      if ( !_isOptional && f.getType != _clsRaw )
        sys.error( "Field '" + name + "' has type '" + f.getType.getName + "' but expected is '" + _clsRaw.getName + "'." )
      f.setAccessible( true )
      f
    } catch {
      case ex: NoSuchFieldException => sys.error( "No field '" + name + "' exists in class '" + parent.cls.getName + "'." )
    }

  /**
   * Logical name of this field. The logical name is the database name if set
   * or otherwise the physical field name.
   */
  lazy val logicalName: String = if ( this.dbName == None ) this.name else this.dbName.get

  /**
   * Checks if the given value is valid for this field and can be persisted. By default,
   * this method provides no implementation. Please override if a special validation is required.
   *
   * @param value the value to be checked
   * @throws RuntimeException if the checked value is not persistable
   */
  protected def validate( value: Option[TRawType] ): Unit = {}

  /**
   * Returns the current field value.
   *
   * @param instance the entity from which to retrieve the value
   */
  protected[sds] def getValue( instance: STEntity[_] ): Option[TRawType] = {
    if ( this._isOptional ) {
      this._field.get( instance ).asInstanceOf[Option[TRawType]]
    } else
      Option( this._field.get( instance ).asInstanceOf[TRawType] )
  }

  /**
   * Sets the value of this field in the source STEntity to the given GAE Entity.
   *
   * @param target the GAE Entity to which to set the field's value as property
   * @param src the STEntity from which to read the source value
   */
  protected[sds] def setToGae( target: Entity, src: STEntity[_] ) {
    // get the actual field value, if Option[] is used, then extract the actual value out of
    // the Option. None is considered as being null on the GAE side.
    //
    val valueSds: TRawType = if ( this._isOptional ) {
      val optValueSds: Option[TRawType] = this._field.get( src ).asInstanceOf[Option[TRawType]]
      if ( optValueSds == None )
        null.asInstanceOf[TRawType]
      else
        optValueSds.get
    } else
      this._field.get( src ).asInstanceOf[TRawType]

    if ( valueSds == null )
      setGaeProperty( target, null.asInstanceOf[TGaeType] )
    else {
      validate( Some( valueSds ) )
      setGaeProperty( target, convertSdsToGae( valueSds ) )
    }
  }

  /**
   * Sets the given value to the GAE datastore entity.
   *
   * @param ent the Entity to which to set the value
   * @param value the value to be set
   */
  protected def setGaeProperty( ent: Entity, value: TGaeType ) {
    if ( this.indexed )
      ent.setProperty( this.logicalName, value )
    else
      ent.setUnindexedProperty( this.logicalName, value )
  }

  /**
   * Initialises this field from the GAE.
   *
   * @param target the target instance for which to set the field's value
   * @param src the GAE entity providing the value for this field
   */
  protected[sds] def setFromGae( target: STEntity[_], src: Entity ) {
    val valueGae: TGaeType = src.getProperty( this.logicalName ).asInstanceOf[TGaeType]

    // handle null value
    //
    if ( valueGae == null ) {
      if ( this._isOptional )
        this._field.set( target, None )
      else
        sys.error( "Received null for field'" + this.parent.cls.getName + "#" + this._field.getName + "' but type is not Option[_]!" )
    }
    else {
      // non-null values
      //
      val valueSds = convertGaeToSds( valueGae )
      this._field.set( target, if ( this._isOptional ) Some( valueSds ) else valueSds )
    }
  }

  /**
   * Converts the value from the GAE type into the SDS type. By default, if both types
   * are equal, the passed in value is simply returned. Otherwise, an exception is thrown.
   * Fields are expected to override this method if GAE and SDS types are different.
   *
   * @param value the value to be transformed from the GAE to the SDS type
   */
  protected[sds] def convertGaeToSds( value: TGaeType ): TRawType = {
    if ( this._clsGae == this._clsRaw )
      value.asInstanceOf[TRawType]
    else
      sys.error( "Method convertGaeToSds not overridden by " + getClass().getName + " although GAE and SDS types are different." )
  }

  /**
   * Converts the value from the SDS type into the GAE type. By default, if both types
   * are equal, the passed in value is simply returned. Otherwise, an exception is thrown.
   * Fields are expected to override this method if GAE and SDS types are different.
   *
   * @param value the value to be transformed from the SDS to the GAE type
   */
  protected[sds] def convertSdsToGae( value: TRawType ): TGaeType = {
    if ( this._clsGae == this._clsRaw )
      value.asInstanceOf[TGaeType]
    else
      sys.error( "Method convertSdsToGae not overridden by " + getClass().getName + " although GAE and SDS types are different." )
  }
}
